package com.jeesite.maven.plugin.compressor;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.GZIPOutputStream;

import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;

import com.yahoo.platform.yui.compressor.CssCompressor;
import com.yahoo.platform.yui.compressor.JavaScriptCompressor;

/**
 * Apply compression on JS and CSS (using YUI Compressor + Closure Compiler).
 *
 * @author David Bernard
 * @created 2007-08-28
 * @author ThinkGem 
 * @upgrade 2020-3-9
 * @threadSafe
 */
@Mojo(name = "compress", defaultPhase = LifecyclePhase.PROCESS_RESOURCES, threadSafe = false)
public class CompressorMojo extends MojoSupport {

	/**
	 * Read the input file using "encoding".
	 *
	 * @ parameter property="file.encoding" default-value="UTF-8"
	 */
	@Parameter(property = "encoding", defaultValue = "UTF-8")
	private String encoding;

	/**
	 * The output filename suffix.
	 *
	 * @ parameter property="maven.compressor.suffix" default-value="-min"
	 */
	@Parameter(property = "suffix", defaultValue = "-min")
	private String suffix;

	/**
	 * If no "suffix" must be add to output filename (maven's configuration manage empty suffix like default).
	 *
	 * @ parameter property="maven.compressor.nosuffix" default-value="false"
	 */
	@Parameter(property = "nosuffix", defaultValue = "false")
	private boolean nosuffix;

	/**
	 * Insert line breaks in output after the specified column number.
	 *
	 * @ parameter property="maven.compressor.linebreakpos" default-value="-1"
	 */
	@Parameter(property = "linebreakpos", defaultValue = "-1")
	private int linebreakpos;

	/**
	 * [js only] No compression
	 *
	 * @ parameter property="maven.compressor.nocompress" default-value="false"
	 */
	@Parameter(property = "nocompress", defaultValue = "false")
	private boolean nocompress;

	/**
	 * [js only] Minify only, do not obfuscate.
	 *
	 * @ parameter property="maven.compressor.nomunge" default-value="false"
	 */
	@Parameter(property = "nomunge", defaultValue = "false")
	private boolean nomunge;

	/**
	 * [js only] Preserve unnecessary semicolons.
	 *
	 * @ parameter property="maven.compressor.preserveAllSemiColons" default-value="false"
	 */
	@Parameter(property = "preserveAllSemiColons", defaultValue = "false")
	private boolean preserveAllSemiColons;

	/**
	 * [js only] disable all micro optimizations.
	 *
	 * @ parameter property="maven.compressor.disableOptimizations" default-value="false"
	 */
	@Parameter(property = "disableOptimizations", defaultValue = "false")
	private boolean disableOptimizations;

	/**
	 * force the compression of every files, else if compressed file already exists and is younger than source file, nothing is done.
	 *
	 * @ parameter property="maven.compressor.force" default-value="false"
	 */
	@Parameter(property = "force", defaultValue = "false")
	private boolean force;

	/**
	 * a list of aggregation/concatenation to do after processing, for example to create big js files that contain several small js files. Aggregation
	 * could be done on any type of file (js, css, ...).
	 *
	 * @ parameter
	 */
	@Parameter(property = "aggregations")
	private Aggregation[] aggregations;

	/**
	 * request to create a gzipped version of the yuicompressed/aggregation files.
	 *
	 * @ parameter property="maven.compressor.gzip" default-value="false"
	 */
	@Parameter(property = "gzip", defaultValue = "false")
	private boolean gzip;

	/**
	 * show statistics (compression ratio).
	 *
	 * @ parameter property="maven.compressor.statistics" default-value="true"
	 */
	@Parameter(property = "statistics", defaultValue = "true")
	private boolean statistics;

	/**
	 * aggregate files before minify 
	 * 
	 * @ parameter property="maven.compressor.preProcessAggregates" default-value="false"
	 */
	@Parameter(property = "preProcessAggregates", defaultValue = "false")
	private boolean preProcessAggregates;

	/**
	 * use the input file as output when the compressed file is larger than the original 
	 * 
	 * @ parameter property="maven.compressor.useSmallestFile"
	 * default-value="false"
	 */
	@Parameter(property = "useSmallestFile", defaultValue = "false")
	private boolean useSmallestFile;
	
	/**
	 * Specifies the compilation level to use. --compilation_level
	 * 
	 * Options: BUNDLE, WHITESPACE_ONLY, SIMPLE (default), ADVANCED
	 */
	@Parameter(property = "compilationLevel", defaultValue = "SIMPLE")
	private String compilationLevel;

	private long inSizeTotal;
	private long outSizeTotal;

	@Override
	protected String[] getDefaultIncludes() throws Exception {
		return new String[] { "**/*.css", "**/*.js" };
	}

	@Override
	public void beforeProcess() throws Exception {
		if (nosuffix) {
			suffix = "";
		}

		if (preProcessAggregates)
			aggregate();
	}

	@Override
	protected void afterProcess() throws Exception {
		if (statistics && (inSizeTotal > 0)) {
			getLog().info(String.format("total input (%db) -> output (%db)[%d%%]",
					inSizeTotal, outSizeTotal, ((outSizeTotal * 100) / inSizeTotal)));
		}

		if (!preProcessAggregates)
			aggregate();
	}

	private void aggregate() throws Exception {
		if (aggregations != null) {
			Set<File> previouslyIncludedFiles = new HashSet<File>();
			for (Aggregation aggregation : aggregations) {
				getLog().info("generate aggregation : " + aggregation.output);
				Collection<File> aggregatedFiles = aggregation.run(previouslyIncludedFiles, buildContext);
				previouslyIncludedFiles.addAll(aggregatedFiles);

				File gzipped = gzipIfRequested(aggregation.output);
				if (statistics) {
					if (gzipped != null) {
						getLog().info(String.format("%s (%db) -> %s (%db)[%d%%]", aggregation.output.getName(), aggregation.output.length(),
								gzipped.getName(), gzipped.length(), ratioOfSize(aggregation.output.length(), gzipped.length())));
					} else if (aggregation.output.exists()) {
						getLog().info(String.format("%s (%db)", aggregation.output.getName(), aggregation.output.length()));
					} else {
						getLog().warn(String.format("%s not created", aggregation.output.getName()));
					}
				}
			}
		}
	}

	@Override
	protected void processFile(SourceFile src) throws Exception {

		File inFile = src.toFile();
		File outFile = src.toDestFile(suffix);
		getLog().debug("compress file :" + inFile + " to " + outFile);

		getLog().debug("only compress if input file is younger than existing output file");
		if (!force && outFile.exists() && (outFile.lastModified() > inFile.lastModified())) {
			if (getLog().isInfoEnabled()) {
				getLog().info("nothing to do, " + outFile + " is younger than original, use 'force' option or clean your target");
			}
			return;
		}
		
		long inSize = 0;
		if (statistics) {
			inSize = inFile.length();
			inSizeTotal += inSize;
		}

		File outFileTmp = new File(outFile.getAbsolutePath() + ".tmp");
		FileUtils.forceDelete(outFileTmp);
		
		if (".js".equalsIgnoreCase(src.getExtension())) {
			googleCompress(compilationLevel, inFile, outFileTmp);
			if (inFile.length() < outFileTmp.length()) {
				long outFileTmpOrig = outFileTmp.length();
				googleCompress("WHITESPACE_ONLY", inFile, outFileTmp);
				getLog().info(String.format("%s (%db) -> %s (%db orig:%db)[whitespace only mode]", inFile.getName(), 
						inSize, outFile.getName(), outFileTmp.length(), outFileTmpOrig));
			}
		} else {
			yuiCompress(src, inFile, outFile, outFileTmp);
		}

		//getLog().info("useSmallestFile: " + useSmallestFile + ", inFile: " + inFile.length() + ", outFileTmp: " + outFileTmp.length());
		boolean outputIgnored = useSmallestFile && inFile.length() < outFileTmp.length();
		if (outputIgnored) {
			FileUtils.copyFile(inFile, outFile);
			getLog().info(String.format("%s (%db) -> %s (%db)[output greater than input, using original instead]", inFile.getName(), 
					inSize, outFile.getName(), outFileTmp.length()));
			FileUtils.forceDelete(outFileTmp);
		} else {
			FileUtils.forceDelete(outFile);
			FileUtils.rename(outFileTmp, outFile);
			buildContext.refresh(outFile);
			buildContext.refresh(outFileTmp);
		}
		//getLog().info("output: " + outFile);

		long outSize = 0;
		File gzipped = gzipIfRequested(outFile);
		if (statistics) {
			outSize = outFile.length();
			outSizeTotal += outSize;

			String fileStatistics;
			if (outputIgnored) {
				fileStatistics = String.format("%s (%db) -> %s (%db)[compressed output discarded (exceeded input size)]", inFile.getName(),
						inSize, outFile.getName(), outSize);
			} else {
				fileStatistics = String.format("%s (%db) -> %s (%db)[%d%%]", inFile.getName(), inSize, outFile.getName(), outSize,
						ratioOfSize(inSize, outSize));
			}

			if (gzipped != null) {
				fileStatistics = fileStatistics
						+ String.format(" -> %s (%db)[%d%%]", gzipped.getName(), gzipped.length(), ratioOfSize(inSize, gzipped.length()));
			}
			getLog().info(fileStatistics);
		}
	}

	private void googleCompress(String compilationLevel, File inFile, File outFileTmp) throws Exception {
		List<String> a = new ArrayList<>();
		a.add("--compilation_level=" + compilationLevel);
		if (!jswarn) {
			a.add("--warning_level=QUIET");
		}
		a.add("--js=" + inFile.getAbsolutePath());
		a.add("--js_output_file=" + outFileTmp.getAbsolutePath());
		GoogleCompiler.run(a, getLog());
	}

	private void yuiCompress(SourceFile src, File inFile, File outFile, File outFileTmp) throws Exception {
		InputStreamReader in = null;
		OutputStreamWriter out = null;
		try {
			in = new InputStreamReader(new FileInputStream(inFile), encoding);
			if (!outFile.getParentFile().exists() && !outFile.getParentFile().mkdirs()) {
				throw new MojoExecutionException("Cannot create resource output directory: " + outFile.getParentFile());
			}
			getLog().debug("use a temporary outputfile (in case in == out)");

			getLog().debug("start compression");
			out = new OutputStreamWriter(buildContext.newFileOutputStream(outFileTmp), encoding);
			if (nocompress) {
				getLog().info("No compression is enabled");
				IOUtil.copy(in, out);
			} if (".js".equalsIgnoreCase(src.getExtension())) {
				JavaScriptCompressor compressor = new JavaScriptCompressor(in, jsErrorReporter);
				compressor.compress(out, linebreakpos, !nomunge, jswarn, preserveAllSemiColons, disableOptimizations);
			} else if (".css".equalsIgnoreCase(src.getExtension())) {
				try {
					CssCompressor compressor = new CssCompressor(in);
					compressor.compress(out, linebreakpos);
				} catch (IllegalArgumentException e) {
					throw new IllegalArgumentException(
							"Unexpected characters found in CSS file. Ensure that the CSS file does not contain '$', and try again", e);
				}
			}
			getLog().debug("end compression");
		} finally {
			IOUtil.close(in);
			IOUtil.close(out);
		}
	}

	protected File gzipIfRequested(File file) throws Exception {
		if (!gzip || (file == null) || (!file.exists())) {
			return null;
		}
		if (".gz".equalsIgnoreCase(FileUtils.getExtension(file.getName()))) {
			return null;
		}
		File gzipped = new File(file.getAbsolutePath() + ".gz");
		getLog().info(String.format("create gzip version : %s", gzipped.getName()));
		GZIPOutputStream out = null;
		FileInputStream in = null;
		try {
			out = new GZIPOutputStream(buildContext.newFileOutputStream(gzipped));
			in = new FileInputStream(file);
			IOUtil.copy(in, out);
		} finally {
			IOUtil.close(in);
			IOUtil.close(out);
		}
		return gzipped;
	}

	protected long ratioOfSize(long file100, long fileX) throws Exception {
		long v100 = Math.max(file100, 1);
		long vX = Math.max(fileX, 1);
		return (vX * 100) / v100;
	}
}
